package com.google.gwt.user.rebind.rpc;

import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.Map;

import com.google.gwt.core.client.GWT;
import com.google.gwt.core.client.impl.Impl;
import com.google.gwt.core.ext.BadPropertyValueException;
import com.google.gwt.core.ext.ConfigurationProperty;
import com.google.gwt.core.ext.GeneratorContext;
import com.google.gwt.core.ext.PropertyOracle;
import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.core.ext.typeinfo.JClassType;
import com.google.gwt.core.ext.typeinfo.JMethod;
import com.google.gwt.core.ext.typeinfo.JPackage;
import com.google.gwt.core.ext.typeinfo.JType;
import com.google.gwt.core.ext.typeinfo.TypeOracle;
import com.google.gwt.dev.javac.TypeOracleMediator;
import com.google.gwt.user.client.rpc.IsSerializable;
import com.google.gwt.user.client.rpc.SerializationException;
import com.google.gwt.user.client.rpc.SerializationStreamWriter;
import com.google.gwt.user.client.rpc.impl.RemoteServiceProxy;
import com.google.gwt.user.client.rpc.impl.RpcStatsContext;
import com.google.gwt.user.client.rpc.impl.RequestCallbackAdapter.ResponseReader;
import com.google.gwt.user.linker.rpc.RpcLogArtifact;
import com.google.gwt.user.rebind.ClassSourceFileComposerFactory;
import com.google.gwt.user.rebind.SourceWriter;
import com.google.gwt.user.server.rpc.impl.TypeNameObfuscator;
import com.seventhdawn.gwt.rcx.client.annotation.ClientProxySuperclass;
import com.seventhdawn.gwt.rcx.client.annotation.CustomSerializableRoots;

/**
 * Creates an asynchronous client-side implementation of a
 * {@link com.google.gwt.user.client.rpc.RemoteService RemoteService} interface
 * as well as the necessary type and field serializers.
 * <p>
 * This class overrides the behavior of certain {@link ProxyCreator} methods:
 * <ul>
 * <li>
 * {@link #getSourceWriter(TreeLogger, GeneratorContext, JClassType)
 * getSourceWriter} regarding the
 * {@link com.seventhdawn.gwt.rcx.client.annotation.ClientProxySuperclass
 * ClientProxySuperclass} annotation
 * <li>
 * {@link #addRoots(TreeLogger, TypeOracle, SerializableTypeOracleBuilder, SerializableTypeOracleBuilder)
 * addRoots} regarding the
 * {@link com.seventhdawn.gwt.rcx.client.annotation.CustomSerializableRoots
 * CustomSerializableRoots} annotation
 * </ul>
 * 
 * @author vojtech.szocs
 */
public class UberProxyCreator extends ProxyCreator {

    Class<? extends RemoteServiceProxy> getProxySuperclass(TreeLogger logger) {
        ClientProxySuperclass rpcSuperAnnotation = serviceIntf.getAnnotation(ClientProxySuperclass.class);

        if (rpcSuperAnnotation != null) {
            return rpcSuperAnnotation.value();
        } else {
            logger.log(TreeLogger.DEBUG, "RemoteService proxy superclass was not defined with ClientProxySuperclass annotation, "
                    + "falling back to default proxy implementation: " + RemoteServiceProxy.class.getSimpleName());
            return RemoteServiceProxy.class;
        }
    }

    SourceWriter getSourceWriter(TreeLogger logger, GeneratorContext ctx, JClassType serviceAsync) {
        JPackage serviceIntfPkg = serviceAsync.getPackage();
        String packageName = serviceIntfPkg == null ? "" : serviceIntfPkg.getName();
        PrintWriter printWriter = ctx.tryCreate(logger, packageName, getProxySimpleName());
        if (printWriter == null) {
            return null;
        }

        ClassSourceFileComposerFactory composerFactory = new ClassSourceFileComposerFactory(packageName, getProxySimpleName());

        String[] imports = new String[] { getStreamWriterClass().getCanonicalName(), SerializationStreamWriter.class.getCanonicalName(),
                GWT.class.getCanonicalName(), ResponseReader.class.getCanonicalName(), SerializationException.class.getCanonicalName(),
                Impl.class.getCanonicalName(), RpcStatsContext.class.getCanonicalName() };
        for (String imp : imports) {
            composerFactory.addImport(imp);
        }

        // set client-side proxy superclass
        Class<? extends RemoteServiceProxy> rpcSuperclass = getProxySuperclass(logger);
        composerFactory.setSuperclass(rpcSuperclass.getSimpleName());
        composerFactory.addImport(rpcSuperclass.getCanonicalName());

        composerFactory.addImplementedInterface(serviceAsync.getErasedType().getQualifiedSourceName());

        return composerFactory.createSourceWriter(ctx, printWriter);
    }

    @Override
    protected void addRoots(TreeLogger logger, TypeOracle typeOracle, SerializableTypeOracleBuilder typesSentFromBrowserBuilder,
            SerializableTypeOracleBuilder typesSentToBrowserBuilder) throws UnableToCompleteException {
        super.addRoots(logger, typeOracle, typesSentFromBrowserBuilder, typesSentToBrowserBuilder);
        addCustomRoots(logger, typeOracle, typesSentFromBrowserBuilder, typesSentToBrowserBuilder);
    }

    void addCustomRoots(TreeLogger logger, TypeOracle typeOracle, SerializableTypeOracleBuilder typesSentFromBrowserBuilder,
            SerializableTypeOracleBuilder typesSentToBrowserBuilder) {
        CustomSerializableRoots customRootAnnotation = serviceIntf.getAnnotation(CustomSerializableRoots.class);

        if (customRootAnnotation != null) {
            Class<? extends IsSerializable>[] customRoots = customRootAnnotation.value();
            logger = logger.branch(TreeLogger.DEBUG, "Analyzing custom serializable roots");

            for (Class<? extends IsSerializable> root : customRoots) {
                TreeLogger rootLogger = logger.branch(TreeLogger.DEBUG, "Custom root type: " + root.getCanonicalName());
                JType rootType = typeOracle.findType(root.getName());

                if (rootType != null) {
                    typesSentFromBrowserBuilder.addRootType(rootLogger, rootType);
                    typesSentToBrowserBuilder.addRootType(rootLogger, rootType);
                } else {
                    rootLogger.log(TreeLogger.WARN, "Unable to find metadata");
                }
            }
        }
    }

    public UberProxyCreator(JClassType serviceIntf) {
        super(serviceIntf);
    }

    // following code is copied (or overrided and copied) from ProxyCreator

    private static final String PROXY_SUFFIX = "_Proxy";

    private boolean elideTypeNames;

    private String getProxyQualifiedName() {
        String[] name = Shared.synthesizeTopLevelClassName(serviceIntf, PROXY_SUFFIX);
        return name[0].length() == 0 ? name[1] : name[0] + "." + name[1];
    }

    @Override
    public String create(TreeLogger logger, GeneratorContext context) throws UnableToCompleteException {
        TypeOracle typeOracle = context.getTypeOracle();

        JClassType serviceAsync = typeOracle.findType(serviceIntf.getQualifiedSourceName() + "Async");
        if (serviceAsync == null) {
            logger.branch(TreeLogger.ERROR, "Could not find an asynchronous version for the service interface "
                    + serviceIntf.getQualifiedSourceName(), null);
            RemoteServiceAsyncValidator.logValidAsyncInterfaceDeclaration(logger, serviceIntf);
            throw new UnableToCompleteException();
        }

        SourceWriter srcWriter = getSourceWriter(logger, context, serviceAsync);
        if (srcWriter == null) {
            return getProxyQualifiedName();
        }

        // Make sure that the async and synchronous versions of the
        // RemoteService agree with one another
        RemoteServiceAsyncValidator rsav = new RemoteServiceAsyncValidator(logger, typeOracle);
        Map<JMethod, JMethod> syncMethToAsyncMethMap = rsav.validate(logger, serviceIntf, serviceAsync);

        final PropertyOracle propertyOracle = context.getPropertyOracle();

        // Load the blacklist/whitelist
        TypeFilter blacklistTypeFilter = new BlacklistTypeFilter(logger, propertyOracle);

        // Determine the set of serializable types
        SerializableTypeOracleBuilder typesSentFromBrowserBuilder = new SerializableTypeOracleBuilder(logger, propertyOracle, typeOracle);
        typesSentFromBrowserBuilder.setTypeFilter(blacklistTypeFilter);
        SerializableTypeOracleBuilder typesSentToBrowserBuilder = new SerializableTypeOracleBuilder(logger, propertyOracle, typeOracle);
        typesSentToBrowserBuilder.setTypeFilter(blacklistTypeFilter);

        addRoots(logger, typeOracle, typesSentFromBrowserBuilder, typesSentToBrowserBuilder);

        try {
            ConfigurationProperty prop = context.getPropertyOracle().getConfigurationProperty(TypeSerializerCreator.GWT_ELIDE_TYPE_NAMES_FROM_RPC);
            elideTypeNames = Boolean.parseBoolean(prop.getValues().get(0));
        } catch (BadPropertyValueException e) {
            logger.log(TreeLogger.ERROR, "Configuration property " + TypeSerializerCreator.GWT_ELIDE_TYPE_NAMES_FROM_RPC
                    + " is not defined. Is RemoteService.gwt.xml inherited?");
            throw new UnableToCompleteException();
        }

        // Decide what types to send in each direction.
        // Log the decisions to a string that will be written later in this
        // method
        SerializableTypeOracle typesSentFromBrowser;
        SerializableTypeOracle typesSentToBrowser;
        String rpcLog;
        {
            StringWriter stringWriter = new StringWriter();
            PrintWriter writer = new PrintWriter(stringWriter);

            typesSentFromBrowserBuilder.setLogOutputWriter(writer);
            typesSentToBrowserBuilder.setLogOutputWriter(writer);

            writer.write("====================================\n");
            writer.write("Types potentially sent from browser:\n");
            writer.write("====================================\n\n");
            writer.flush();
            typesSentFromBrowser = typesSentFromBrowserBuilder.build(logger);

            writer.write("===================================\n");
            writer.write("Types potentially sent from server:\n");
            writer.write("===================================\n\n");
            writer.flush();
            typesSentToBrowser = typesSentToBrowserBuilder.build(logger);

            writer.close();
            rpcLog = stringWriter.toString();
        }

        generateTypeHandlers(logger, context, typesSentFromBrowser, typesSentToBrowser);

        String serializationPolicyStrongName = writeSerializationPolicyFile(logger, context, typesSentFromBrowser, typesSentToBrowser);

        String remoteServiceInterfaceName = elideTypeNames ? TypeNameObfuscator.SERVICE_INTERFACE_ID
                : TypeOracleMediator.computeBinaryClassName(serviceIntf);
        generateProxyFields(srcWriter, typesSentFromBrowser, serializationPolicyStrongName, remoteServiceInterfaceName);

        generateProxyContructor(srcWriter);

        generateProxyMethods(srcWriter, typesSentFromBrowser, syncMethToAsyncMethMap);

        if (elideTypeNames) {
            generateStreamWriterOverride(srcWriter);
        }

        srcWriter.commit(logger);

        // Create an artifact explaining STOB's decisions. It will be emitted by
        // RpcLogLinker
        context.commitArtifact(logger, new RpcLogArtifact(serviceIntf.getQualifiedSourceName(), serializationPolicyStrongName, rpcLog));

        return getProxyQualifiedName();
    }

}
